タップしたらイベントを下位に伝搬しつつ消えるボタンの作り方


概要

簡単にやる方法があったので書く。

リポジトリはこちら。


Raycaster

https://github.com/sassembla/Raycaster



需要とか説明とか

なんでもいいからどこか押したら消えるウィンドウ

(しかも押した時の動作は打ち消さないでほしいみたいな狂ったニーズがあるとき)

に使える。


こんなGUIがあったとして、

スクリーンショット 2017-03-22 22.41.16.png


一番手前のが今回の話の対象で、これもボタンになっていて、

・押す(touchEndとかUp)と消える

・もしも押した位置が下のボタンにも入っていれば、ボタンを押したことにする


というキツいニーズを持っているとしよう。


そんなんねーだろって思うんだけど、ある。

こういうGUIを作りたいケース。

スクリーンショット 2017-03-22 22.49.39.png


まあ、さっきまで控えめだった半透明ボタンが画面に覆いかぶさってる。

これは、

・手前の白いボタンをタップしたらボタンについてるイベントが着火

・それ以外のところ(半透明)をクリックしたら、消えつつ下のイベントが着火


みたいな感じで、ようは

「白いボタンを押してイベントを発生させる or それ以外の範囲を押すと半透明や白いボタンが消えつつ下のボタンを押したことにする」

というような、排他的な領域のタッチ取得 + イベント発生に使える。


半透明のやつが全透明だったらもっとわかりやすいかも。

スクリーンショット 2017-03-22 22.51.34.png

この、青い丸で四角く括られてる部位を触ると、白いボタンが消えてこの領域も消えて、下のボタンを押したことになる、みたいなのが理想ですと。



理想のイベント発生順

・半透明のところをクリックすると、半透明と白いボタンが消える

・もし消すときにタップした位置にボタンがあったら、それを発動させる



実装の情報

UnityでのuGUIは便利なもんで、

EventSystemを使っていてくれる場合は非常に簡単にタッチイベントを遮ったり偽造したりすることができる。


具体的には以下の方法を使う。


1.でかいボタンを作る(半透明のやつ)

2.ボタンに次のようなコードを貼る

using System.Collections.Generic;

using UnityEngine;

using UnityEngine.EventSystems;


public class RaycastPreventor : MonoBehaviour {


    public void CatchTap () {

        // ここで、発生したイベントはなんなんだろう。というのを型で判断できそう。今後見てみる。


        this.gameObject.SetActive(false);


        var tapPos = new Vector3();

        if (0 < Input.touchCount) {

            tapPos = Input.touches[0].position;

        } else {

            tapPos = Input.mousePosition;

        }

        

        // 新規のpointerイベントを生成する

        var newPointerEvent = new PointerEventData(EventSystem.current);

        

        // ここでの位置のセットが必須。

        newPointerEvent.position = tapPos;

        

        var objectsHit = new List<RaycastResult>();

        EventSystem.current.RaycastAll(newPointerEvent, objectsHit);


        foreach (var objectHit in objectsHit) {

            // 同種のイベントを受けられるやつだけを通す。

            if (!ExecuteEvents.CanHandleEvent<IPointerClickHandler>(objectHit.gameObject)) {

                continue;

            }


            // 最初の一つにだけ、イベントを送り込む。

            ExecuteEvents.Execute<IPointerClickHandler>(objectHit.gameObject, newPointerEvent, (handler, eventData) => handler.OnPointerClick((PointerEventData)eventData));


            //一件通ったらもう着火する必要がないので、逃げる。

            break;

        }

    }

}


3.ボタンのOnClickイベントのとこに、CatchTapメソッドを貼る。

スクリーンショット 2017-03-22 22.54.44.png


これで、実装自体は終わり。やってることの紹介は以下。


実装内容紹介

次のような動作になってる。


1.CatchTap着火

2.this.gameObject.SetActive(false); でこのオブジェクト自体を無効化

3.tapPosを取得して、pointerEventを新規作成したものの位置に適応し、同じ位置でのイベントを作成

4.EventSystem.current.RaycastAll(newPointerEvent, objectsHit); で、このイベントにヒットするオブジェクト一覧を取得

5.取得したオブジェクトに対して、該当するイベントを受けられるかどうかチェック

6.イベントを受けられる物体を見つけたら、最初の一つだけにイベントを着火して完


調べるのに苦労したんだけどうーーーむ、APIドキュメントにサンプルコードが少ないのがキツいのかなあ。

継承関係とかはそりゃまあAPIドキュメント見ればわかるんだけど、利用例が一切ないコードを型情報からつなぎ合わせたり

欲しい動きをしそうなメソッドを探すのが大変だった。


GraphicRaycasterを使って云々したりもできるので便利。